Imports System.ComponentModel
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Linq
Imports System.Windows.Forms
Imports DotSpatial.Controls
Imports DotSpatial.Controls.Header
Imports DotSpatial.Data
Imports DotSpatial.Projections
Imports DotSpatial.Topology

Public Class clsWebMap

#Region "Constants and Fields"

    Private ReadOnly WebMercProj As ProjectionInfo = ProjectionInfo.FromEsriString(KnownCoordinateSystems.Projected.World.WebMercator.ToEsriString())
    Private ReadOnly Wgs84Proj As ProjectionInfo = ProjectionInfo.FromEsriString(KnownCoordinateSystems.Geographic.World.WGS1984.ToEsriString())

    'reference to the main application and its UI items
    Private _baseMapLayer As MapImageLayer

    Private _basemapImage As Bitmap

    Private WithEvents _bw As BackgroundWorker

    Private _emptyProvider As ServiceProvider

    Private _featureSetLayer As IMapFeatureLayer

    Private _Opacity As Short = 100

    Private _provider As ServiceProvider

    Private _tileManager As TileManager

    Private _GisMap As Map

#End Region

#Region "Public Methods"

    Public Sub New(GisMap As Map)
        _GisMap = GisMap
        _tileManager = New TileManager
        _bw = New BackgroundWorker With {.WorkerSupportsCancellation = True, .WorkerReportsProgress = True}
    End Sub

    ''' <summary>
    ''' Return a list of the names of the webmap service providers currently supported
    ''' </summary>
    Public Function GetDefaultServiceProviders() As List(Of String)
        Dim lst As New Generic.List(Of String)
        _emptyProvider = New ServiceProvider("None", Nothing)
        lst.Add(_emptyProvider.Name)
        For Each provider As ServiceProvider In ServiceProvider.GetDefaultServiceProviders
            lst.Add(provider.Name)
        Next
        Return lst
    End Function

    ''' <summary>
    ''' Set or get the name of the currently selected service provider
    ''' </summary>
    ''' <value>The standard name of the provider</value>
    ''' <remarks>Remember to reset this to "None" if a basemap layer is removed using the legend</remarks>
    Public Property ServiceProviderName As String
        Set(value As String)
            If value = "None" Then
                DisableBasemapLayer()
            Else
                For Each s As ServiceProvider In ServiceProvider.GetDefaultServiceProviders
                    If s.Name = value Then
                        _provider = s
                        EnableBasemapFetching(s.Name, s.Url)
                        Exit For
                    End If
                Next
            End If
        End Set
        Get
            Return _provider.Name
        End Get
    End Property

    ''' <summary>
    ''' Set opacity of webmap layer; ranges from 0 to 100
    ''' </summary>
    Public Property Opacity As Short
        Get
            Return _Opacity
        End Get
        Set(value As Short)
            _Opacity = value
            ChangeBasemapOpacity(_Opacity)
        End Set
    End Property

    ''' <summary>
    ''' Force the webmap to refresh (required when loading from .gis file)
    ''' </summary>
    Public Sub Refresh()
        While _bw.IsBusy
            Threading.Thread.Sleep(500)
            Application.DoEvents()
        End While
        If _bw.IsBusy <> True Then
            _bw.RunWorkerAsync()
        End If
    End Sub

    Public Event Progress(Text As String)

#End Region

#Region "Methods"

    ''' <summary>
    ''' Returns the input bitmap after making it transparent
    ''' </summary>
    ''' <returns>Opacity-modified bitmap of the basemap image</returns>
    Private Shared Function GetTransparentBasemapImage(originalImage As Bitmap, opacity As Integer) As Bitmap
        If originalImage Is Nothing Then
            Return Nothing
        End If

        Try
            Dim newImage = New Bitmap(originalImage.Width, originalImage.Height)
            Using gfxPic = Graphics.FromImage(newImage)
                Using iaPic = New ImageAttributes()
                    Dim cmxPic = New ColorMatrix()
                    cmxPic.Matrix33 = opacity / 100.0
                    iaPic.SetColorMatrix(cmxPic, ColorMatrixFlag.[Default], ColorAdjustType.Bitmap)
                    gfxPic.DrawImage(originalImage, New Rectangle(0, 0, originalImage.Width, originalImage.Height), 0, 0, originalImage.Width, originalImage.Height, _
                        GraphicsUnit.Pixel, iaPic)
                End Using
            End Using

            Return newImage
        Catch
            Return originalImage
        End Try
    End Function

    ''' <summary>
    ''' Inserts the _baseMapLayer below point layers and line layers
    ''' </summary>
    Private Sub AddBasemapLayerToMap()
        'check all top-level map point layers, line layers
        Dim groupOrLayerIndex As Integer = FindPointOrLineLayerIndex(_GisMap.MapFrame)
        If groupOrLayerIndex >= 0 Then
            _GisMap.Layers.Insert(groupOrLayerIndex, _baseMapLayer)
        Else
            _GisMap.Layers.Add(_baseMapLayer)
        End If
    End Sub

    Private Function FindPointOrLineLayerIndex(group As IMapGroup) As Integer
        Dim index As Integer = -1

        For Each layer As IMapLayer In group.Layers
            index += 1

            Dim childGroup As IMapGroup = TryCast(layer, IMapGroup)
            If childGroup IsNot Nothing Then
                Dim groupIndex As Integer = FindPointOrLineLayerIndex(childGroup)
                If groupIndex <> -1 Then
                    Return index
                End If
            End If

            If TypeOf layer Is IMapPointLayer OrElse TypeOf layer Is IMapLineLayer Then
                Return index
            End If
        Next
        'if no layer was found, return -1
        Return -1
    End Function

    ''' <summary>
    '''
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub BwDoWork(sender As Object, e As DoWorkEventArgs) Handles _bw.DoWork
        Try

            Dim worker = TryCast(sender, BackgroundWorker)

            If worker IsNot Nothing AndAlso _baseMapLayer IsNot Nothing Then
                If worker.CancellationPending Then
                    e.Cancel = True
                Else
                    worker.ReportProgress(50)
                    UpdateStichedBasemap()
                End If
            End If

        Catch ex As Exception

            ErrorMsg(, ex)

        End Try

    End Sub

    Private Sub BwProgressChanged(sender As Object, e As ProgressChangedEventArgs) Handles _bw.ProgressChanged
        ' Do we know what what our progress completion percent is (instead of 50)?
        ' TODO: make this localizable
        'RaiseEvent Progress("Loading Basemap ...")
    End Sub

    ''' <summary>
    '''
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub BwRunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles _bw.RunWorkerCompleted
        Dim map = TryCast(_GisMap, Map)
        If map IsNot Nothing Then
            map.MapFrame.Invalidate()
        End If
        RaiseEvent Progress("")
    End Sub

    ''' <summary>
    ''' Changes the opacity of the current basemap image and invalidates the map
    ''' </summary>
    ''' <param name="opacity">The opacity as a value between 0 and 100, inclusive.</param>
    Private Sub ChangeBasemapOpacity(opacity As Short)
        MapFrameExtentsChanged(Me, New ExtentArgs(_GisMap.ViewExtents))
    End Sub

    ''' <summary>
    '''
    ''' </summary>
    Private Sub DisableBasemapLayer()
        RemoveBasemapLayer(_baseMapLayer)
        RemoveBasemapLayer(_featureSetLayer)

        _baseMapLayer = Nothing
        _basemapImage = Nothing
        _featureSetLayer = Nothing

        RemoveHandler _GisMap.MapFrame.ViewExtentsChanged, AddressOf MapFrameExtentsChanged
    End Sub

    Private Sub ForceMaxExtentZoom()
        Dim webMerc As ProjectionInfo = KnownCoordinateSystems.Projected.World.WebMercator

        'special case when there are no other layers in the map. Set map projection to WebMercator and zoom to max ext.
        MapFrameProjectionHelper.ReprojectMapFrame(_GisMap.MapFrame, webMerc.ToEsriString())

        ' modifying the view extents didn't get the job done, so we are creating a new featureset.
        Dim fs = New FeatureSet(FeatureType.Point)
        fs.Features.Add(New Coordinate(TileCalculator.MinWebMercX, TileCalculator.MinWebMercY))
        fs.Features.Add(New Coordinate(TileCalculator.MaxWebMercX, TileCalculator.MaxWebMercY))

        fs.Projection = _GisMap.Projection
        _featureSetLayer = _GisMap.Layers.Add(fs)

        ' hide the points that we are adding.
        _featureSetLayer.LegendItemVisible = False

        _GisMap.ZoomToMaxExtent()
    End Sub

    Private Sub EnableBasemapFetching(tileServerName As String, tileServerUrl As String)
        Dim webMerc As ProjectionInfo = KnownCoordinateSystems.Projected.World.WebMercator

        ' Zoom out as much as possible if there are no other layers.
        'reproject any existing layer in non-webMercator projection.
        'if (_GisMap.Layers.Count == 0 || _GisMap.Projection != webMerc)
        If _GisMap.Layers.Count = 0 Then
            ForceMaxExtentZoom()
        ElseIf Not _GisMap.Projection.Equals(webMerc) Then
            'get original view extents
            RaiseEvent Progress("Reprojecting Map Layers...")
            Dim viewExtentXY As Double() = {_GisMap.ViewExtents.MinX, _GisMap.ViewExtents.MinY, _GisMap.ViewExtents.MaxX, _GisMap.ViewExtents.MaxY}
            Dim viewExtentZ As Double() = {0.0, 0.0}

            'reproject view extents
            Reproject.ReprojectPoints(viewExtentXY, viewExtentZ, _GisMap.Projection, webMerc, 0, 2)

            'set the new map extents
            _GisMap.ViewExtents = New Extent(viewExtentXY)

            'if projection is not WebMercator - reproject all layers:
            MapFrameProjectionHelper.ReprojectMapFrame(_GisMap.MapFrame, webMerc.ToEsriString())

            RaiseEvent Progress("Loading Basemap...")
        End If

        ' Special case for WMS
        If tileServerName.Equals("WMS", StringComparison.InvariantCultureIgnoreCase) Then
            Using wmsDialog = New WMSServerParameters()
                If wmsDialog.ShowDialog() <> DialogResult.OK Then
                    Return
                End If
                _tileManager.WmsServerInfo = wmsDialog.WmsServerInfo
            End Using
        End If

        ' Other is a custom service
        If tileServerName.Equals("Other", StringComparison.InvariantCultureIgnoreCase) Then
            tileServerUrl = Interaction.InputBox("Please provide the Url for the service.", DefaultResponse:="http://tiles.virtualearth.net/tiles/h{key}.jpeg?g=461&mkt=en-us&n=z")

            ' Let the user cancel...
            If [String].IsNullOrWhiteSpace(tileServerUrl) Then
                Return
            End If
        End If

        EnableBasemapLayer()

        _tileManager.ChangeService(tileServerName, tileServerUrl)

        If _bw.IsBusy <> True Then
            _bw.RunWorkerAsync()
        End If
    End Sub

    Private Sub BaseMapLayerRemoveItem(sender As Object, e As EventArgs)
        _baseMapLayer = Nothing
        _provider = _emptyProvider
        RemoveHandler _GisMap.MapFrame.ViewExtentsChanged, AddressOf MapFrameExtentsChanged
    End Sub

    ''' <summary>
    '''
    ''' </summary>
    Private Sub EnableBasemapLayer()
        Try

            If _baseMapLayer Is Nothing Then
                'Need to first initialize and add the basemap layer synchronously (it will fail if done in
                ' another thread.

                'First create a temporary imageData with an Envelope (otherwise adding to the map will fail)
                Dim tempImageData = New InRamImageData(My.Resources.NoDataTile, _GisMap.Extent) 'New Extent(1, 1, 2, 2))

                _baseMapLayer = New MapImageLayer(tempImageData)
                _baseMapLayer.Projection = _GisMap.Projection
                _baseMapLayer.LegendText = "Online Basemap"
                AddHandler _baseMapLayer.RemoveItem, AddressOf BaseMapLayerRemoveItem

                AddBasemapLayerToMap()
            End If

            AddHandler _GisMap.MapFrame.ViewExtentsChanged, AddressOf MapFrameExtentsChanged

        Catch ex As Exception

            ErrorMsg(, ex)

        End Try

    End Sub

    Private Sub EnsureMapProjectionIsWebMercator()
        If Not _GisMap.Projection.Equals(KnownCoordinateSystems.Projected.World.WebMercator) Then
            _GisMap.Projection = KnownCoordinateSystems.Projected.World.WebMercator
        End If
    End Sub

    ''' <summary>
    '''
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub MapFrameExtentsChanged(sender As Object, e As ExtentArgs)
        If _bw.IsBusy <> True Then
            _bw.RunWorkerAsync()
        End If
    End Sub

    ''' <summary>
    ''' Finds and removes the online basemap layer
    ''' </summary>
    ''' <param name="Layer"></param>
    Private Sub RemoveBasemapLayer(Layer As IMapLayer)
        'attempt to remove from the top-level
        If _GisMap.Layers.Remove(Layer) Then
            Return
        End If

        'Remove from other groups if the user has moved it
        _GisMap.Layers.OfType(Of IMapGroup)().Any(Function(grp) grp.Remove(Layer))
    End Sub

    ''' <summary>
    ''' Main method of this plugin: gets the tiles from the TileManager, stitches them together, and adds the layer to the map.
    ''' </summary>
    Private Sub UpdateStichedBasemap()
        Try

            Dim map = TryCast(_GisMap, Map)
            If map IsNot Nothing Then
                Dim rectangle = map.Bounds
                Dim webMercExtent = map.ViewExtents

                'Clip the reported Web Merc Envelope to be within possible Web Merc extents
                '  This fixes an issue with Reproject returning bad results for very large (impossible) web merc extents reported from the Map
                Dim webMercTopLeftX = TileCalculator.Clip(webMercExtent.MinX, TileCalculator.MinWebMercX, TileCalculator.MaxWebMercX)
                Dim webMercTopLeftY = TileCalculator.Clip(webMercExtent.MaxY, TileCalculator.MinWebMercY, TileCalculator.MaxWebMercY)
                Dim webMercBtmRightX = TileCalculator.Clip(webMercExtent.MaxX, TileCalculator.MinWebMercX, TileCalculator.MaxWebMercX)
                Dim webMercBtmRightY = TileCalculator.Clip(webMercExtent.MinY, TileCalculator.MinWebMercY, TileCalculator.MaxWebMercY)

                'Get the web mercator vertices of the current map view
                Dim mapVertices As Double() = {webMercTopLeftX, webMercTopLeftY, webMercBtmRightX, webMercBtmRightY}

                Dim z As Double() = {0, 0}

                'Reproject from web mercator to WGS1984 geographic
                Reproject.ReprojectPoints(mapVertices, z, WebMercProj, Wgs84Proj, 0, mapVertices.Length \ 2)
                Dim geogEnv = New Envelope(mapVertices(0), mapVertices(2), mapVertices(1), mapVertices(3))

                'Grab the tiles
                Dim tiles = _tileManager.GetTiles(geogEnv, rectangle)

                'Stitch them into a single image
                Dim stitchedBasemap = TileCalculator.StitchTiles(tiles)

                _basemapImage = stitchedBasemap

                stitchedBasemap = GetTransparentBasemapImage(stitchedBasemap, _Opacity)

                Dim tileImage = New InRamImageData(stitchedBasemap)

                'Tiles will have often slightly different bounds from what we are displaying on screen
                ' so we need to get the top left and bottom right tiles' bounds to get the proper extent
                ' of the tiled image
                Dim topLeftTile = tiles(0, 0)
                Dim bottomRightTile = tiles(tiles.GetLength(0) - 1, tiles.GetLength(1) - 1)

                Dim tileVertices As Double() = {topLeftTile.Envelope.TopLeft().X, topLeftTile.Envelope.TopLeft().Y, bottomRightTile.Envelope.BottomRight().X, bottomRightTile.Envelope.BottomRight().Y}

                'Reproject from WGS1984 geographic coordinates to web mercator so we can show on the map
                Reproject.ReprojectPoints(tileVertices, z, Wgs84Proj, WebMercProj, 0, tileVertices.Length / 2)

                tileImage.Bounds = New RasterBounds(stitchedBasemap.Height, stitchedBasemap.Width, New Extent(tileVertices(0), tileVertices(3), tileVertices(2), tileVertices(1)))

                _baseMapLayer.Image = tileImage
            End If


        Catch ex As Exception

            RaiseEvent Progress("Error retrieving map data!")

        End Try

    End Sub

#End Region
End Class
